fix: emit structural repair patches immediately and make engine normalization the sole repairer - #3246
fix: emit structural repair patches immediately and make engine normalization the sole repairer#3246christianhg wants to merge 2 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Bundle Stats✅ No significant changes. All scenario measurements (7)🗺️
Significant means at least 1.0 KB and 1% gzip, or at least 5 ms and 10% import time. |
225ddef to
ff45522
Compare
🦋 Changeset detectedLatest commit: 2c75642 The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
ff45522 to
30ff4fe
Compare
918ffda to
bbace4a
Compare
bbace4a to
f2d5db0
Compare
f2d5db0 to
11039dd
Compare
11039dd to
95972c0
Compare
95972c0 to
1aaf52d
Compare
1aaf52d to
eb27e3d
Compare
eb27e3d to
257636d
Compare
257636d to
f142ba1
Compare
…lization the sole repairer
When a value entered the editor with structurally invalid content
(blocks or children without a `_key`, missing or empty `children`
arrays, duplicate keys), the engine repaired it right away but the
repair patches were parked: the editor machine's `pristine` state
deferred `internal.patch` and `mutation` events produced under
normalization until the first local edit flipped it to `dirty`.
Parked patches go stale under concurrent remote traffic (a sibling
insert shifts the indices a parked repair addresses, so the flush
re-keys the wrong node). The mutation batcher additionally dropped
pending work on a read-only unmount, `validateValue` repaired some
of the same defects before ingestion with its own patch shapes (its
insert branch never reported them), and a block missing its `_key`
failed validation into the invalid-value flow.
The parking machinery is deleted rather than gated: `writing` emits
`internal.patch`/`mutation` directly (the `pristine`/`dirty` split,
the normalization guard, and the conflict-discard actions are gone),
and events deferred during setup flush FIFO when setup completes.
Nothing keys on document pristineness anymore. The mutation batcher
keeps one gate, and it is a different kind: `patch` events relay
immediately regardless of read-only state, but mutation bulks hold
while the editor is read-only and flush on the first tick after it
becomes editable. Studio and Canvas flip the editor read-only when
the connection drops and both throw on mutations delivered while
read-only, so an ungated flush loses the edit typed just before the
flip (pinned by a test whose host listener rejects mutations while
read-only, red on the ungated batcher). The cleanup flush is
unconditional, so pending work is handed over on unmount instead of
dropped. `isDeferringMutations` counts only bulks holding a patch
accumulated while the editor was editable: repair bulks held by a
read-only editor do not latch the sync machine's busy guard, so a
read-only viewer of a malformed document keeps applying incoming
value updates (pinned red on the latching version). Held bulks
without editable-time patches are culled when inbound state applies
(value sync or remote patches settle, signalled via an emit ordered
after the pass's own repair emissions; a plain call misordered under
reentrant streamed syncs and lost the fresh re-mint, pinned red):
repairs are re-derivable, so a superseded repair is dropped rather
than delivered against state that moved past it. A remote-patches
batch culls only when it changed engine state: a full snapshot always
supersedes or re-mints a held repair, but a no-op patch batch
supersedes nothing and ran no normalization to re-mint, so it keeps
the held repair (pinned red on the ungated cull); the batch's
`changed` flag accumulates across patches instead of reflecting only
the last one. One consequence: an operation whose patches span an
inbound application splits into two mutation events.
`validateValue`'s `autoResolve` resolutions are deleted with their
reporting machinery (`applyAutoResolution`, the sync actor's
out-of-band `patch` channel); validation passes mechanically fixable
shapes through and engine normalization repairs them on intake,
emitting engine-shaped patches with `origin: 'local'` (minimal `set`
on the minted `_key`, placeholder-span `insert` before
`children[0]`). `updateBlock` gains a wholesale-set fallback for raw
input: children arrays containing keyless children, or a raw empty
`children` replacing populated children, are set wholesale instead
of reconciled per child, so no `{_key: undefined}` path segment is
ever applied; normalization then mints the keys.
The invalid-value flow remains for defects that need a human (a
non-object block, an unknown `_type`, a span whose `text` is not a
string), and its resolutions address the defective block truthfully:
block anchors resolve through `nodeSegment` (numeric index when the
key is unusable, instead of `{_key: undefined}`, which keyed
matching would resolve to the first keyless sibling), and
`validateValue` takes the block's base index so paths and
descriptions carry the block's document position rather than its
position in the one-block slice `syncBlock` validates (previously
always 0: applying such a resolution rewrote the first block).
Deliberate changes: orphaned `markDefs` are no longer pruned at
intake (the engine's prune is local-only by the two-class model);
they go when a local edit next dirties the block, as a `set` of the
filtered array. Sync `insert` operations carry the raw block, and
normalization repairs follow as their own operations. A snapshot
echoing a still-keyless copy of an already-repaired block re-mints
on the next sync: whole-value sync has no key to correlate an echo
by. Cosmetic normalization (span merging, `markDefs`/`style`/`marks`
defaults) still materializes at the first local edit.
`InvalidValueResolution.autoResolve` is deprecated, never set.
…e normalization the sole repairer
f142ba1 to
2c75642
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2c75642. Configure here.
| // Every `patch` event marks a change the editor produced on its own, | ||
| // a user edit or an intake/normalization repair, never a remote | ||
| // application bouncing its own patches back; either way, the store | ||
| // now lags the editor until the next mutation flush. |
There was a problem hiding this comment.
Read-only repairs stall value sync
Medium Severity
Intake repair patch events now relay while the editor is read-only, and SDKValuePlugin still treats every patch as an in-flight local write. The machine moves to local write and waits for a mutation that the batcher holds until the editor becomes editable, so whole-value remote updates stay parked in pending sync for the life of a read-only session.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 2c75642. Configure here.


Opening a document whose value contains structurally invalid content (blocks or children without a
_key, missing or emptychildrenarrays, duplicate keys) now emits the fixing patches as soon as the value settles. Previously they were held back until the first local edit. Read-only editors emit them too: individual patches relay immediately regardless of read-only state, since a host mirroring them for display has nothing to reject. Mutations, the debounced batches hosts persist, wait for the editor to become editable before delivering, because a host following the documentedonChangecontract rejects mutations against a read-only document; a held mutation that a newer snapshot supersedes is dropped instead of delivered late, and pending mutations are handed over on unmount instead of dropped, so an edit typed just before a read-only flip and unmount is never lost. A block missing its_keyis repaired like any other mechanical defect instead of triggering the invalid-value flow, which now only fires for defects that need a human, with resolution paths anchored at the defective block's actual position.The repair patches take the editor's own shapes: a minted
_keyis a minimalseton the_keyfield, an empty text block gets its placeholder span as aninsertbeforechildren[0], and all repair patches carryorigin: 'local'. OrphanedmarkDefsare no longer pruned when a value enters the editor; they are pruned when a local edit next touches the block, as asetof the filteredmarkDefsarray.InvalidValueResolution.autoResolveis deprecated and never set.Accepted behavior, recorded on the tracking tickets: a snapshot echoing a still-keyless copy of an already-repaired block re-mints on the next sync (whole-value sync has no key to correlate an echo by), and concurrent openers of the same malformed document each emit their own repair, converging by last write for key mints.
Note
High Risk
Changes core value sync, patch/mutation delivery, read-only/unmount flushing, and invalid-value vs normalization boundaries—areas that affect persistence, collaboration, and host
onChangecontracts.Overview
Structural repairs (missing
_key, emptychildren, duplicate keys, etc.) now emitpatchevents immediately when a value settles—including on open and in read-only mode—instead of waiting for the first local edit. Engine normalization is the only path that applies those fixes;validateValueno longer auto-resolves mechanical defects, andInvalidValueResolution.autoResolveis deprecated. Human-facinginvalid valueis reserved for problems that need a user, with resolution patches anchored bynodeSegment(key or document index).Mutation batching is split from patches: patches always relay at once; debounced
mutationevents stay held while read-only (and flush on editable or unmount, including read-only teardown). A generation-basedculldrops stale repair bulks after inbound sync or applied remote patches, wired vianotifyInboundStateAppliedandinbound state appliedfrom the sync machine.isDeferringMutationsonly reflects user edits worth protecting from snapshot clobber.The editor machine drops the pristine/dirty patch gating and sync-time patch forwarding through the sync actor; setup now flushes deferred patches/mutations at
ready. Sync no longer tracks read-only for auto-resolution;updateBlockwholesale-replaceschildrenwhen keys are missing or unusable. OrphanedmarkDefsare no longer stripped on intake—they are pruned on the next local edit to the block.Reviewed by Cursor Bugbot for commit 2c75642. Bugbot is set up for automated code reviews on this repo. Configure here.